package com.sos.comm
{
	import com.sos.comm.CallEvent;
	import com.sos.logging.Logger;
	
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	
	/**
	 *  
	 * @author ultima
	 * 
	 */	
	public class CallSequence extends EventDispatcher
	{
		
		// -------------------------------------------------------------------------------
		// CONSTANTS
		// -------------------------------------------------------------------------------
		
		private static const	INVALID_INDEX:int					= -1;
		
		// Logging
		public static const		LOG_MARKER:String						= Call.LOG_MARKER + "SEQUENCE > ";
		public static const		DEBUG_KEY:String						= "comm.call.sequence";
		
		
		// -------------------------------------------------------------------------------
		// FIELDS
		// -------------------------------------------------------------------------------
		
		private var _current:int;
		private var _calls:Vector.<Call>;
		private var _status:CallSequenceStatus;
		private var _cancelled:Boolean;
		private var _params:Dictionary;
		
		// -------------------------------------------------------------------------------
		// CONSTRUCTOR
		// -------------------------------------------------------------------------------
		
		public function CallSequence()
		{
			// Init
			_current	= INVALID_INDEX;
			_status 	= CallSequenceStatus.READY;
			_calls 		= new Vector.<Call>();
			_params		= new Dictionary(true);
			_cancelled	= false;
		}
		
		// -------------------------------------------------------------------------------
		// PROPERTIES
		// -------------------------------------------------------------------------------
		
		public function get callsCount():int
		{
			return _calls.length;
		}
		
		public function get currentCallIndex():int
		{
			return _current;
		}
		
		public function get currentCall():Call
		{
			var call:Call = null;
			if (_current > -1)
			{
				call = _calls[_current];	
			}
			return call;	
		}
		
		[Bindable(event="sequence_status_changed")]
		public function get executed():Boolean
		{
			return	_status == CallSequenceStatus.COMPLETED ||
					_status == CallSequenceStatus.FAILED;
		}
		
		[Bindable(event="sequence_status_changed")]
		public function get running():Boolean
		{
			return	_status == CallSequenceStatus.RUNNING;
		}
		
		[Bindable(event="sequence_status_changed")]
		public function get executable():Boolean
		{
			return 	_status == CallSequenceStatus.READY &&
					_calls.length > 0 &&
					_current < _calls.length -1;
		}
		
		[Bindable(event="sequence_status_changed")]
		public function get status():CallSequenceStatus
		{
			return _status;
		}
		
		// -------------------------------------------------------------------------------
		// PRIVATE METHODS
		// -------------------------------------------------------------------------------
		
		protected function setStatus(status:CallSequenceStatus):void
		{
			// Check if status changes
			if (_status != status)
			{
				// Retrieve current status
				var oldStatus:CallSequenceStatus = _status;
				// Apply new status
				_status = status;
				// Dispatch events
				dispatchStatusChangedEvent();
				// DEBUG > Trace status change
				if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + " status changed {0} > {1}", oldStatus, _status);
			}
		}
		
		protected function indexOfCall(call:Call):int
		{
			for (var i:int = 0; i < _calls.length; i++)
			{
				if (_calls[i].equals(call)) return i;
			}
			return INVALID_INDEX;
		}
		
		protected function sequenceExecute():void
		{
			// DEBUG > Sequence execution : START 
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "CallSequence.sequenceExecute() > START");
			// Calls
			var call:Call = _calls[_current];
			var prevCall:Call = _current > 0 ? _calls[_current - 1] : null;
			// Dispatch prepare call event
			dispatchSequenceBeforeExecutionEvent(call, prevCall);
			// Execute current call
			call.execute();
			// DEBUG > Sequence execution : STOP
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "CallSequence.sequenceExecute('{0}') > STOP", call.url);
		}
		
		protected function sequenceCompleted():void
		{
			// DEBUG > Sequence completed : START 
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "CallSequence.sequenceCompleted() > START");
			// Create the results dictionary
			var results:Dictionary = new Dictionary(true);
			// Loop over calls
			for (var i:int=0; i<_calls.length; i++)
			{
				results[_calls[i].url] = _calls[i].data; 
			}
			// Sequence > FAILED
			setStatus(CallSequenceStatus.FAILED);
			// Dispatch sequence completed
			dispatchSequenceCompletedEvent(results);
			// Dispatch sequence stop
			dispatchSequenceStopEvent();
			// DEBUG > Sequence completed : STOP
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "CallSequence.sequenceCompleted() > STOP");
		}
		
		protected function sequenceCancelled():void
		{
			// DEBUG > Sequence cancelled : START 
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "CallSequence.sequenceCancelled() > START");
			// Sequence > FAILED
			setStatus(CallSequenceStatus.FAILED);
			// Dispatch sequence failed
			dispatchSequenceFailedEvent(true, null);
			// Sequence stop
			dispatchSequenceStopEvent();
			// DEBUG > Sequence cancelled : START 
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "CallSequence.sequenceCancelled() > STOP");
		}
		
		protected function sequenceFailed(failedCall:Call):void
		{
			// DEBUG > Sequence failed : START 
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "CallSequence.sequenceFailed() > START");
			// Sequence > FAILED
			setStatus(CallSequenceStatus.FAILED);
			// Dispatch sequence failed
			dispatchSequenceFailedEvent(false, failedCall);
			// Sequence stop
			dispatchSequenceStopEvent();
			// DEBUG > Sequence failed : STOP
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "CallSequence.sequenceFailed() > STOP");
		}
		
		// EVENT HANDLERS
		
		protected function addCallEventListeners(call:Call):void
		{
			call.addEventListener(CallEvent.EVENT_CALL_SUCCESS, 	callSuccessHandler);
			call.addEventListener(CallEvent.EVENT_CALL_ERROR,		callErrorHandler);
			call.addEventListener(CallEvent.EVENT_CALL_FAIL, 		callFailedHandler);
		}
		
		protected function removeCallEventListeners(call:Call):void
		{
			call.removeEventListener(CallEvent.EVENT_CALL_SUCCESS, callSuccessHandler);
			call.removeEventListener(CallEvent.EVENT_CALL_ERROR,	callErrorHandler);
			call.removeEventListener(CallEvent.EVENT_CALL_FAIL, 	callFailedHandler);
		}
		
		protected function callSuccessHandler(e:CallSuccessEvent):void
		{
			// DEBUG > Sequence call success handler : START 
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "CallSequence.callSuccessHandler() > START");
			// Dispatch progress event
			dispatchSequenceProgressEvent(e.call, _current, _calls.length - 1);
			// Dispatch after execution event
			dispatchSequenceAfterExecutionEvent(e.call);
			// Check if to stop execution
			if (!_cancelled)
			{
				// Check if there are other calls in the sequence 
				if (_current < (_calls.length - 1))
				{
					// Increment call counter
					_current++;
					// Next call
					sequenceExecute();
				}
				else
				{
					// Sequence completed
					sequenceCompleted();
				}
			}
			else
			{
				// Sequence cancelled by the user
				sequenceCancelled();
			}
			// DEBUG > Sequence call success handler : STOP 
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "CallSequence.callSuccessHandler() > STOP");
		}
		
		protected function callErrorHandler(e:CallErrorEvent):void
		{		
			sequenceFailed(e.call);
		}
		
		protected function callFailedHandler(e:CallFailEvent):void
		{
			sequenceFailed(e.call);
		}
		
		// EVENT DISPATCH
		
		protected function dispatchStatusChangedEvent():void
		{
			dispatchEvent(new CallSequenceEvent(CallSequenceEvent.STATUS_CHANGED, this));
		}
		
		protected function dispatchSequenceStartEvent():void
		{
			dispatchEvent(new CallSequenceEvent(CallSequenceEvent.START, this));
		}
		
		protected function dispatchSequenceStopEvent():void
		{
			dispatchEvent(new CallSequenceEvent(CallSequenceEvent.STOP, this));
		}
		
		protected function dispatchSequenceProgressEvent(call:Call, index:int, total:int):void
		{
			dispatchEvent(new CallSequenceProgressEvent(this, call, index, total));
		}
		
		protected function dispatchSequenceBeforeExecutionEvent(call:Call, prevCall:Call):void
		{
			dispatchEvent(new CallSequenceBeforeExecutionEvent(this, call, prevCall));
		}
		
		protected function dispatchSequenceAfterExecutionEvent(call:Call):void
		{
			dispatchEvent(new CallSequenceAfterExecutionEvent(this, call));
		}
		
		protected function dispatchSequenceCompletedEvent(results:Dictionary):void
		{
			dispatchEvent(new CallSequenceCompletedEvent(this, results));
		}
		
		protected function dispatchSequenceFailedEvent(cancelled:Boolean, failedCall:Call):void
		{
			dispatchEvent(new CallSequenceFailedEvent(this, cancelled, failedCall));
		}
		
		// -------------------------------------------------------------------------------
		// PUBLIC METHODS
		// -------------------------------------------------------------------------------
		
		public function execute():void
		{
			// Sequence status check
			if (_status != CallSequenceStatus.READY) throw new Error("Cannot executed the sequence when the sequence status is not READY");
			// Calls check
			if (callsCount == 0) throw new Error("Cannot executed the sequence, no calls in the sequence");
			// Reset current call index
			_current = 0;
			// Dispatche the sequence start event
			dispatchSequenceStartEvent();
			// Sequence > RUNNING
			setStatus(CallSequenceStatus.RUNNING);
			// Runs the first call
			sequenceExecute();
		}
		
		public function cancel():void
		{
			// Sequence status check
			if (_status != CallSequenceStatus.RUNNING) throw new Error("Cannot cancel the sequence when the sequence status is not READY");
			// Flags the cancellation of the sequence
			_cancelled = true;
		}
		
		public function getParameter(key:String):Object
		{
			var param:Object = null;
			if (_params.hasOwnProperty(key))
			{
				param = _params[key]; 	
			}
			return param;
		}
		
		public function setParameter(key:String, value:Object):void
		{
			_params[key] = value;
		}
		
		public function getCall(index:int):Call
		{
			return _calls[index];
		}
		
		public function addCall(call:Call):void
		{
			// Sequence status check
			if (_status != CallSequenceStatus.READY) throw new Error("Cannot add a call when the sequence status is not READY");
			// Call status check
			if (call.status != CallStatus.READY) throw new Error("Cannot add a call when his in status is not READY");
			// Instance check
			// LT_TODO
			// Listener add
			addCallEventListeners(call);
			// Cdd the call
			_calls.push(call);
		}
		
		public function removeCall(call:Call):void
		{
			// Get index of the call to remove
			var index:int = indexOfCall(call);
			// remove the call by index
			removeCallByIndex(index);
		}
		
		public function removeCallByIndex(index:int):void
		{
			// Sequence status check
			if (_status != CallSequenceStatus.READY) throw new Error("Cannot remove the call when the sequence is not in status READY");
			// Check if the Call index to remove
			if (index == INVALID_INDEX) throw new Error("Cannot remove call not contained in the sequence");
			// Removes the listeners from the call
			removeCallEventListeners(_calls[index]);
			// Removes the call from the sequence
			_calls.splice(index, 1);
		}

		public function clear(removeCalls:Boolean = false):void
		{
			// Sequence status check
			if (_status == CallSequenceStatus.RUNNING) throw new Error("Cannot clear the sequence when the sequence is RUNNING");
			// Loops over calls
			for (var i:int = 0; i < _calls.length; i++)
			{
				if (removeCalls)
				{
					removeCallByIndex(i);	
				}
				else
				{
					_calls[i].clear();
				}
			}
			// Loops over params
			for (var key:Object in _params)
			{
				key = null;
			}
			// Reset status and counters
			_current = INVALID_INDEX;
			// force status tu READY
			setStatus(CallSequenceStatus.READY);
		}
		
	}
}